perf(storer): reuse buffers and eliminate per-chunk allocations in reserve sample - #5612
gacevicljubisa wants to merge 7 commits into
Conversation
Sampling is about to start reading chunks into a buffer that each worker reuses. Nothing today checks that the bytes handed back in a SampleItem are still the bytes of that chunk, so a reused buffer would silently hand the redistribution proof the contents of some later chunk. assertValidSample now checks two things for every item: that ChunkData still reproduces ChunkAddress, and that no two items share a backing array. Every existing sample test picks both up. Also add the rulers for the work that follows. BenchmarkReserveSample1k keeps its name and behaviour so the recorded baseline stays comparable; its body moves to a helper that BenchmarkReserveSample10k reuses over a ten times larger reserve. BenchmarkChunkStoreGet measures a single chunk read, split into a variant that builds the ChunkStore handle per call as the sampler does today and one that hoists it, so the cost of the handle alone is visible. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KFUseFQ8rhp6N9X6YS7pbq
db.ChunkStore() builds three objects every time it is called, and the sampler called it once per chunk. On a testnet node that is 2.3 million handles per round, thrown away immediately. Hoist it to one per worker. The handle is deliberately not shared across workers: the read-only chunk store makes no thread-safety promise, and three allocations per worker is already nothing next to three per chunk. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KFUseFQ8rhp6N9X6YS7pbq
444d89d to
841bbaf
Compare
Both ReserveSample benchmarks fill their reserve with chunk.GenerateValidRandomChunkAt, which produces content-addressed chunks only. The SOC branch of transformedAddress is therefore never measured by them, and any work that branch does beyond hashing the wrapped CAC is invisible. Benchmark the function directly, one case per chunk type, through a shim in export_test.go. The shim takes a swarm.Chunk and is held to that signature on purpose so the same benchmark source can be run against branches whose internal transformedAddress is shaped differently; only the shim changes between them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KFUseFQ8rhp6N9X6YS7pbq
|
|
||
| type ChunkStore interface { | ||
| Getter | ||
| GetterInto |
There was a problem hiding this comment.
Not sure if we need to add it to the ChunkStore interface. Since we only use ReadOnlyChunkStore for sampling, we can only add it to the interface below. I think we can even avoid adding it to transaction.ChunkStore also.
There was a problem hiding this comment.
Dropping it makes ReadOnlyChunkStore no longer a subset of ChunkStore, which breaks the test storages and the mock storer that return the same value as both. Then even more changes are needed...
| return ch, err | ||
| } | ||
|
|
||
| func (c *chunkStoreTrx) GetInto(ctx context.Context, addr swarm.Address, buf []byte) (n int, err error) { |
There was a problem hiding this comment.
Check comment above. Since we only need ReadOnlyChunkStore to provide this function, we can avoid adding it here.
| continue | ||
| } | ||
|
|
||
| ch, err := phase3ChunkStore.Get(ctx, item.chunkAddress) |
There was a problem hiding this comment.
instead ctx shouldn't we also use in this Phase 3 gCtx ?
| continue | ||
| } | ||
|
|
||
| ch, err := phase3ChunkStore.Get(ctx, item.chunkAddress) |
There was a problem hiding this comment.
phase3ChunkStore.Get runs before the consensusTime check: stamp.Timestamp() is already in memory from Step 1. Loading the 4096-byte chunk from Sharky before verifying stamp.Timestamp() <= consensusTime causes completely wasted disk reads for every chunk stamped past consensusTime.
Also, phase3ChunkStore.Get runs before db.validStamp: In Swarm, ValidStamp does not inspect chunk data—it only validates chunk.Address() and chunk.Stamp(). Performing a disk read before verifying whether the stamp is even valid burns I/O for expired or invalid stamps.
| // state and concurrent calls would see flapping values. On platforms where the | ||
| // dispatcher falls back to the goroutine pool (non-linux/amd64 or CPU without | ||
| // AVX2/AVX-512), the SIMD sub-test degrades to a goroutine run. | ||
| func TestSampleVectorCAC(t *testing.T) { |
There was a problem hiding this comment.
Should we also have a TestSampleVectorSOC, because changes were made in that area?
|
|
||
| type ChunkStore interface { | ||
| Getter | ||
| GetterInto |
There was a problem hiding this comment.
GetterInto was added to the required ChunkStore/ReadOnlyChunkStore interfaces, and forgetting.go embeds storage.ChunkStore. DelayedStore and ForgettingStore override Get/Put but not GetInto, so the promoted method forwards straight through — a chunk marked Miss() or Delay()ed comes back normally. Latent today, but it'll bite whoever first routes a read path through GetInto.
| @@ -21,6 +21,14 @@ type Getter interface { | |||
| Get(context.Context, swarm.Address) (swarm.Chunk, error) | |||
| } | |||
|
|
|||
There was a problem hiding this comment.
implementations return a bare fmt.Errorf("chunk store: buffer too small: %d < %d", ...). A caller can't distinguish it from ErrNotFound.
Maybe add:
// ErrBufferTooSmall is returned by GetInto when len(buf) is smaller than the
// stored chunk. Callers may inspect it with errors.Is to retry with a larger buffer.
var ErrBufferTooSmall = errors.New("storage: buffer too small")
Checklist
Description
This PR addresses per-chunk allocations and buffer overhead in
ReserveSample(issue #5174):escapes.
actually qualify for the sample candidate set.
BenchmarkReserveSample10k.
A/B on
bee-light-testnet, two nodes with identical config (no SIMD, verbosity 3, 1500m CPU, storage radius 2,isWarmingUp: false). Sample triggered viaGET /rchash/2/<own-overlay>/<anchor2>; allocations attributed withpprof -diff_basefocused on theReserveSamplesubtree.b6537ea2chunkstore.readChunkbuffertransformedAddress(control)bmt.Write(control)Total allocation churn per sample round: 24.65 GB → 10.30 GB over 2.405M chunks.
Open API Spec Version Changes (if applicable)
Motivation and Context (Optional)
Related Issue (Optional)
Screenshots (if appropriate):
AI Disclosure